#include "dicom/dmv_db.h"

#include <assert.h>
#include <gwbase/base/logging.h>

namespace dicom {

DmvDB::DmvDB() : opended_(false) {}
DmvDB::~DmvDB() {}

Status DmvDB::Open(const std::string& path) {
  assert(!opended_);
  Status s;
  s = db::Database::Open(path, &db_);
  if (!s.ok()) return s;

  EnsureTableCreation();
  return s;
}

void DmvDB::EnsureTableCreation() {
  db_->RunSql(
      "create table if not exists file_tab ("
      "local_id varchar primary key, "
      "cloud_id varchar not null, "
      "type varchar, "
      "name varchar, "
      "createTime integer default 0, "
      "modifiedTime integer default 0, "
      "creator varchar, "
      "parentId varchar not null, "
      "size integer default -1, "
      "star int default 0, "
      "is_delete int default 0, "
      "svgPageCount int default 0, "
      "metaV int default 0, "
      "digest varchar not null, "
      "version int default 0, "
      "newDoc int default 0, "
      "allParentId varchar, "
      "isSepModified int default 0,"
      "hex_key varchar)");

  db_->RunSql(
      "create index if not exists file_tab_pid_idx"
      " on file_tab(parentId)");
  db_->RunSql(
      "create index if not exists file_tab_cloud_id_idx"
      " on file_tab(cloud_id)");

  db_->RunSql(
      "create table if not exists dir_tab ("
      "local_id varchar primary key, "
      "cloud_id varchar not null, "
      "type varchar, "
      "name varchar, "
      "createTime integer default 0, "
      "modifiedTime integer default 0, "
      "creator varchar, "
      "parentId varchar not null, "
      "allParentId varchar, "
      "is_delete int default 0, "
      "star int default 0, "
      "share int default 0, "
      "fileCount int default 0)");

  db_->RunSql(
      "create index if not exists dir_tab_pid_idx"
      " on dir_tab(parentId)");
  db_->RunSql(
      "create index if not exists dir_tab_cloud_id_idx"
      " on dir_tab(cloud_id)");
}

void DmvDB::InsertRootDirNode(const std::string& local_id,
                              const std::string& cloud_id) {
  LockGuard g(mu_);
  DirObject d;
  if (this->GetDirByIDWithLock(local_id, &d) == false) {
    d.local_id = local_id;
    d.cloud_id = cloud_id;
    d.parentId = "";
    d.name = "";
    d.creator = "";
    d.createTime = Timestamp::Now();
    d.modifiedTime = Timestamp::Now();
    UpdateDirWithLock(d);
  }
}

void DmvDB::ReadFileObject(db::StmtPtr& stmt, FileObject* obj) {
  int p = 0;
  obj->local_id = stmt->GetText(p++);
  obj->cloud_id = stmt->GetText(p++);
  obj->type = stmt->GetText(p++);
  obj->name = stmt->GetText(p++);
  obj->createTime = Timestamp(stmt->GetInt64(p++));
  obj->modifiedTime = Timestamp(stmt->GetInt64(p++));
  obj->creator = stmt->GetText(p++);
  obj->parentId = stmt->GetText(p++);
  obj->size = stmt->GetInt64(p++);
  obj->star = stmt->GetInt32(p++);
  obj->is_delete = stmt->GetInt32(p++);
  obj->svgPageCount = stmt->GetInt32(p++);
  obj->metaV = stmt->GetInt32(p++);
  obj->digest = stmt->GetText(p++);
  obj->version = stmt->GetInt32(p++);
  obj->newDoc = stmt->GetInt32(p++);
  obj->allParentId = stmt->GetText(p++);
  obj->isSepModified = stmt->GetInt32(p++);
  obj->hex_key = stmt->GetText(p++);
}

void DmvDB::UpdateFileWithLock(const FileObject& f) {
  const char* sql =
      "replace into file_tab ("
      "local_id, "
      "cloud_id, "
      "type, "
      "name, "
      "createTime, "
      "modifiedTime, "
      "creator, "
      "parentId, "
      "size, "
      "star, "
      "is_delete, "
      "svgPageCount, "
      "metaV, "
      "digest, "
      "version, "
      "newDoc, "
      "allParentId, "
      "isSepModified, "
      "hex_key) "
      "values "
      "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(f.local_id);
  stmt->BindText(f.cloud_id);
  stmt->BindText(f.type);
  stmt->BindText(f.name);
  stmt->BindInt64(f.createTime.MicroSecondsSinceEpoch());
  stmt->BindInt64(f.modifiedTime.MicroSecondsSinceEpoch());
  stmt->BindText(f.creator);
  stmt->BindText(f.parentId);
  stmt->BindInt64(f.size);
  stmt->BindInt32(f.star);
  stmt->BindInt32(f.is_delete);
  stmt->BindInt32(f.svgPageCount);
  stmt->BindInt32(f.metaV);
  stmt->BindText(f.digest);
  stmt->BindInt32(f.version);
  stmt->BindInt32(f.newDoc);
  stmt->BindText(f.allParentId);
  stmt->BindInt32(f.isSepModified);
  stmt->BindText(f.hex_key);
  stmt->Step();
}

void DmvDB::UpdateDirWithLock(const DirObject& d) {
  const char* sql =
      "replace into dir_tab ("
      "local_id, "
      "cloud_id, "
      "type, "
      "name, "
      "createTime, "
      "modifiedTime, "
      "creator, "
      "parentId, "
      "allParentId, "
      "star, "
      "is_delete, "
      "share, "
      "fileCount) "
      "values "
      "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(d.local_id);
  stmt->BindText(d.cloud_id);
  stmt->BindText(d.type);
  stmt->BindText(d.name);
  stmt->BindInt64(d.createTime.MicroSecondsSinceEpoch());
  stmt->BindInt64(d.modifiedTime.MicroSecondsSinceEpoch());
  stmt->BindText(d.creator);
  stmt->BindText(d.parentId);
  stmt->BindInt32(d.star);
  stmt->BindInt32(d.is_delete);
  stmt->BindInt32(d.fileCount);
  stmt->Step();
}

void DmvDB::ReadDirObject(db::StmtPtr& stmt, DirObject* obj) {
  int p = 0;
  obj->local_id = stmt->GetText(p++);
  obj->cloud_id = stmt->GetText(p++);
  obj->type = stmt->GetText(p++);
  obj->name = stmt->GetText(p++);
  obj->createTime = Timestamp(stmt->GetInt64(p++));
  obj->modifiedTime = Timestamp(stmt->GetInt64(p++));
  obj->creator = stmt->GetText(p++);
  obj->parentId = stmt->GetText(p++);
  obj->allParentId = stmt->GetText(p++);
  obj->is_delete = stmt->GetInt32(p++);
  obj->star = stmt->GetInt32(p++);
  obj->share = stmt->GetInt32(p++);
  obj->fileCount = stmt->GetInt32(p++);
}

bool DmvDB::GetSubItems(const std::string& pid, std::vector<DirObject>* dirs,
                        std::vector<FileObject>* files) {
  LockGuard lock(mu_);
  GetDirsByPIDWithLock(pid, dirs);
  GetFilesByPIDWithLock(pid, files);
  return true;
}

bool DmvDB::GetFileByID(const std::string& local_id, FileObject* d) {
  LockGuard lock(mu_);
  return GetFileByIDWithLock(local_id, d);
}

bool DmvDB::GetFileByCloudID(const std::string& cloud_id, FileObject* d) {
  LockGuard lock(mu_);
  db::StmtPtr stmt = db_->Prepare(
      "select * from file_tab where cloud_id = ? and is_delete = 0");
  stmt->BindText(cloud_id);
  stmt->Step();

  if (stmt->eof()) return false;

  ReadFileObject(stmt, d);
  return true;
}

bool DmvDB::GetFileByIDWithLock(const std::string& local_id, FileObject* d) {
  db::StmtPtr stmt = db_->Prepare(
      "select * from file_tab where local_id = ? and is_delete = 0");
  stmt->BindText(local_id);
  stmt->Step();

  if (stmt->eof()) return false;

  ReadFileObject(stmt, d);
  return true;
}

bool DmvDB::GetDirByID(const std::string& local_id, DirObject* d) {
  LockGuard lock(mu_);
  return GetDirByIDWithLock(local_id, d);
}

bool DmvDB::GetDirByCloudId(const std::string& cloud_id, DirObject* d) {
  LockGuard lock(mu_);
  db::StmtPtr stmt = db_->Prepare(
      "select * from dir_tab where cloud_id = ? and is_delete = 0");
  stmt->BindText(cloud_id);
  stmt->Step();

  if (stmt->eof()) return false;

  ReadDirObject(stmt, d);
  return true;
}

bool DmvDB::GetDirByIDWithLock(const std::string& local_id, DirObject* d) {
  db::StmtPtr stmt = db_->Prepare(
      "select * from dir_tab where local_id = ? and is_delete = 0");
  stmt->BindText(local_id);
  stmt->Step();

  if (stmt->eof()) return false;

  ReadDirObject(stmt, d);
  return true;
}

bool DmvDB::GetDirsByPIDWithLock(const std::string& pid,
                                 std::vector<DirObject>* dirs) {
  db::StmtPtr stmt = db_->Prepare(
      "select * from dir_tab where parentId = ? and is_delete = 0");
  stmt->BindText(pid);

  for (stmt->Step(); !stmt->eof(); stmt->Step()) {
    DirObject d;
    ReadDirObject(stmt, &d);
    dirs->push_back(d);
  }

  return true;
}

bool DmvDB::GetFilesByPIDWithLock(const std::string& pid,
                                  std::vector<FileObject>* dirs) {
  db::StmtPtr stmt = db_->Prepare(
      "select * from file_tab where parentId = ? and is_delete = 0");
  stmt->BindText(pid);

  for (stmt->Step(); !stmt->eof(); stmt->Step()) {
    FileObject d;
    ReadFileObject(stmt, &d);
    dirs->push_back(d);
  }

  return true;
}

bool DmvDB::GetSubFileByName(const std::string& pid, const std::string& name,
                             FileObject* fo) {
  LockGuard lock(mu_);
  db::StmtPtr stmt = db_->Prepare(
      "select * from file_tab where parentId = ? and name = ? and is_delete = "
      "0");
  stmt->BindText(pid);
  stmt->BindText(name);
  stmt->Step();
  if (stmt->eof()) return false;
  ReadFileObject(stmt, fo);
  return true;
}

bool DmvDB::GetSubDirByName(const std::string& pid, const std::string& name,
                            DirObject* d) {
  LockGuard lock(mu_);
  db::StmtPtr stmt = db_->Prepare(
      "select * from dir_tab where parentId = ? and name = ? and is_delete = "
      "0");
  stmt->BindText(pid);
  stmt->BindText(name);
  stmt->Step();
  if (stmt->eof()) return false;
  ReadDirObject(stmt, d);
  return true;
}

void DmvDB::DeleteFile(const std::string& local_id) {
  LockGuard lock(mu_);
  db::StmtPtr stmt = db_->Prepare("delete from file_tab where local_id = ?");
  stmt->BindText(local_id);
  stmt->Step();
}

void DmvDB::DeleteFile(const std::string& pid, const std::string& name) {
  LockGuard lock(mu_);
  db::StmtPtr stmt =
      db_->Prepare("delete from file_tab where parentId = ? and name = ?");
  stmt->BindText(pid);
  stmt->BindText(name);
  stmt->Step();
}

void DmvDB::DeleteDir(const std::string& local_id) {
  LOG_INFO << "delete directory: " << local_id;
  LockGuard lock(mu_);
  db_->RunSql("BEGIN;");
  db_->RunSql(StringPrintf("delete from dir_tab where local_id = '%s'",
                           local_id.c_str()));
  db_->RunSql("COMMIT;");
}

void DmvDB::SetDirCloudId(const std::string& local_id,
                          const std::string& cloud_id) {
  LockGuard lock(mu_);
  DirObject d;
  if (GetDirByIDWithLock(local_id, &d) == false) {
    LOG_ERROR << "local id is empty when mapping cloud id to local id:"
              << local_id << " - " << cloud_id;
    return;
  }

  d.cloud_id = cloud_id;
  UpdateDirWithLock(d);
}

void DmvDB::SetFileCloudId(const std::string& local_id,
                           const std::string& cloud_id) {
  LockGuard lock(mu_);
  FileObject fo;
  if (GetFileByIDWithLock(local_id, &fo) == false) {
    LOG_ERROR << "local id is empty when mapping cloud id to local id:"
              << local_id << " - " << cloud_id;
    return;
  }

  fo.cloud_id = cloud_id;
  UpdateFileWithLock(fo);
}

void DmvDB::UpdateDir(const DirObject& d) {
  LockGuard lock(mu_);
  UpdateDirWithLock(d);
}

void DmvDB::UpdateFile(const FileObject& f) {
  LockGuard lock(mu_);
  UpdateFileWithLock(f);
}

std::string DmvDB::GetFileCloudId(const std::string& local_id) {
  FileObject fo;
  if (GetFileByID(local_id, &fo) == false) return "";
  return fo.cloud_id;
}

bool DmvDB::GetDirPath(const std::string& id, std::string* path) {
  LockGuard lock(mu_);
  std::string cur = id;
  std::string p;
  while (cur != "0") {
    DirObject d;
    if (GetDirByIDWithLock(cur, &d) == false) {
      return false;
    }
    p = d.name + "/" + p;
    cur = d.parentId;
  }
  p = "/" + p;
  path->assign(p);
  return true;
}

void DmvDB::UpdateParentDirModifyTime(const std::string& pid) {
  LockGuard lock(mu_);
  const char* sql = "update dir_tab set modifiedTime = ? where local_id = ?";
  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindInt64(Timestamp::Now().MicroSecondsSinceEpoch());
  stmt->BindText(pid);
  stmt->Step();
}

DmvDB::EntryLockHandle DmvDB::LockDentry(const std::string& pid,
                                         const std::string& name) {
  EntryLock* el = NULL;
  {
    LockGuard lock(mu_);
    for (EntryLockList::iterator it = entry_locks_.begin();
         it != entry_locks_.end(); ++it) {
      EntryLock* cur = (*it);
      if (cur->pid == pid && cur->name == name) {
        el = cur;
        break;
      }
    }

    if (el == NULL) {
      el = new EntryLock();
      el->pid = pid;
      el->name = name;
      el->counter = 0;
      entry_locks_.insert(el);
    }

    ++el->counter;
  }

  LOG_DEBUG << "acquire entry lock: " << el << ": " << el->counter << " : "
            << el->name;
  el->lock.Lock();
  return static_cast<EntryLockHandle>(el);
}

void DmvDB::UnlockDentry(EntryLockHandle handle) {
  EntryLock* el = static_cast<EntryLock*>(handle);
  assert(el != NULL);
  el->lock.Unlock();
  {
    LockGuard lock(mu_);
    --el->counter;
    if (el->counter == 0) {
      LOG_DEBUG << "destroy entry lock: " << handle << " : " << el->name;
      entry_locks_.erase(el);
      delete el;
    }
  }
}

DmvDBPtr DmvDBManager::GetDB(const std::string& user) {
  LockGuard lock(mu_);
  UserDBMap::iterator it = dbs_.find(user);
  if (it != dbs_.end()) {
    return it->second;
  }

  Status s;
  DmvDBPtr db(new DmvDB());
  std::string dbdir = conf_->GetDbDir();
  std::string dbpath = path::Join(dbdir, user + "_dmv.db");
  s = db->Open(dbpath);
  if (!s.ok()) {
    LOG_FATAL << "failed to open dmv database";
    db.reset();
    return db;
  }

  dbs_.insert(std::make_pair(user, db));
  return db;
}

EntryLockGuard::EntryLockGuard(DmvDBPtr db, const std::string& pid,
                               const std::string& name)
    : db_(db), h_(db_->LockDentry(pid, name)) {}

EntryLockGuard::~EntryLockGuard() {
  assert(h_ != NULL);
  db_->UnlockDentry(h_);
}

DmvDBManager::DmvDBManager(const DicomConf* conf) : conf_(conf) {}

DmvDBManager::~DmvDBManager() {}

DmvDBManager* NewDmvDBManager(const DicomConf* conf) {
  assert(conf != NULL);
  DmvDBManager* mgr = NULL;
  mgr = new DmvDBManager(conf);
  return mgr;
}
}
